<?php

namespace App\Utils;

use Carbon\Carbon;
use Exception;
use Illuminate\Support\Facades\Cache;
use JetBrains\PhpStorm\ArrayShape;

class SequenceNumberGenerator
{
    /**
     * @param string $serialPrefix
     * @param string $extraPrefix
     * @param int $length
     * @param bool $verify
     * @param string|null $date
     * @return array
     * @throws Exception
     */
    #[ArrayShape(['date' => "string", 'number' => "string", 'serial' => "string"])]
    public static function generate(
        string  $serialPrefix,
        string  $extraPrefix,
        int     $length = 3,
        bool    $verify = true,
        ?string $date = null
    ): array
    {
        $date = $date ?: date('Ymd');
        $serial = self::getSequenceNumber($serialPrefix, $length);
        self::checkSerialBoundary($serial, $length);
        $number = $serialPrefix . $extraPrefix . $date . $serial;
        $number = $verify ?
            $number . self::generateVerifyCode($date . $serial)
            : $number;

        return compact('date', 'number', 'serial');
    }

    /**
     * @param string $prefix
     * @param int $length
     * @return string
     */
    private static function getSequenceNumber(string $prefix, int $length = 3): string
    {
        $sequence = Cache::get($prefix, 1);
        Cache::put($prefix, $sequence + 1, Carbon::tomorrow());

        return str_pad($sequence, $length, 0, STR_PAD_LEFT);
    }

    /**
     * @param string $number
     * @return int
     * @throws Exception
     */
    private static function generateVerifyCode(string $number): int
    {
        if (!is_numeric($number)) {
            throw new Exception('number parameter must be number type');
        }
        $length = strlen($number);
        $sum = 0;
        for ($i = 0; $i < $length; $i++) {
            $sum += $i % 2 == 0 ? array_sum(str_split($number[$i] * 2)) : $number[$i];
        }
        $mod = $sum % 10;

        return $mod ? 10 - $mod : $mod;
    }

    /**
     * @param string $number
     * @param string $prefix
     * @return bool
     * @throws Exception
     */
    public static function verify(string $number, string $prefix = ''): bool
    {
        if ($prefix && !str_starts_with($number, $prefix)) {
            return false;
        }

        return self::generateVerifyCode(substr($number, strlen($prefix), -1)) == substr($number, -1);
    }

    /**
     * @param int $serial
     * @param int $length
     * @throws Exception
     */
    private static function checkSerialBoundary(int $serial, int $length)
    {
        $max = intval(str_pad('', $length, 9));
        if ($serial > $max) {
            throw new Exception("serial number [{$serial}] exceed max value[{$max}]", 1);
        }
    }
}
